/*
 * Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package google_io2011_logad;

import com.google.api.client.auth.oauth2.draft10.AccessTokenErrorResponse;
import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;

import com.google.api.client.googleapis.auth.oauth2.draft10.GoogleAccessTokenRequest.GoogleAuthorizationCodeGrant;
import com.google.api.client.googleapis.auth.oauth2.draft10.GoogleAuthorizationRequestUrl;

import java.awt.Desktop.Action;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URI;

/**
 * A helper for doing OAuth 2.0 authentication of a server-side web application.
 *
 * The OAuth 2.0 Web Application Flow is appropriate for applications that run
 * a web server and can store their client secret securely. This small example
 * uses an in-process web server on answering on localhost to receive requests
 * to make it simple to configure. An actual service would have to have an
 * OAuth redirect URI that was accesible to others.
 *
 * Usage:
 *   // Obtain the secrets needed to identity this application
 *   OAuth2ClientIdentity clientIdentity = new OAuth2ClientIdentity(...);
 *   // Instantiate this helper to authorize the client for a particular set
 *   // of scopes.
 *   OAuth2LocalServerFlow localServerFlow = new OAuth2LocalServerFlow(
 *      clientIdentity, "https://www.googleapis.com/auth/buzz");
 *
 *   // Do the authorization dance and get an access token
 *   String token = localServerFlow.getAccessToken();
 *
 *   // Have each request on a transport use the access token.
 *   AccessProtectedResource.usingAuthorizationHeader(transport, token);
 *
 * @author Google
 */
public class OAuth2LocalServerFlow {

  /**
   * The application credentials and requested scopes.
   */
  OAuth2ClientIdentity clientIdentity;

  /**
   * The scopes you want the user to grant access to.
   */
  final String scopes;

  /**
   * The URL to reach the local server we have started.
   */
  private String localServerUrl;

  /**
   * Create the flow helper.
   *
   * @param clientIdentity The identity credentials for this application.
   * @param scopes The auth scopes we will request access for.
   */
  public OAuth2LocalServerFlow(OAuth2ClientIdentity clientIdentity,
                               String scopes) {
    this.clientIdentity = clientIdentity;
    this.scopes = scopes;
  }

  /**
   * Get an access token authorizing this application to use the given scope
   * on behalf of the user.
   *
   * @return the token.
   */
  public String getAccessToken() throws Exception {

    // Set up a server to receive the response.
    LoginCallbackServer callbackServer = null;
    String verifier = null;
    String tempToken = null;
    try {
      callbackServer = new LoginCallbackServer();
      Log("Starting a local web server to receive our redirect from the Google OAuth server.");
      callbackServer.start();
      localServerUrl = callbackServer.getCallbackUrl();

      // Generate the URL which talks to the Google OAuth server such that it
      // will identify
      String authorizationUrl = new GoogleAuthorizationRequestUrl(
          clientIdentity.clientId,
          localServerUrl,
          scopes).build();
      Log("OAuth: Built the URL for an OAuth request: \n    "
          + authorizationUrl);

      Log("OAuth: The user must visit that URL in their browser.");
      launchInBrowser(authorizationUrl);

      Log("OAuth:. When the user accepts the request, the OAuth server\n"
          + "will redirect to our local server.  Waiting...");
      verifier = callbackServer.waitForVerifier(tempToken);
      Log("OAuth:. We got the verification code: " + verifier);
    } finally {
      if (callbackServer != null) {
        callbackServer.stop();
      }
    }
    // Exchange the verifier code for a short lived access token

    Log("OAuth: Asking the OAuth server to turn the verification code\n"
        + "into an access token...");
    String token = exchangeCodeForAccessToken(verifier);
    Log("OAuth: Got the access token: " + token);
    return token;
  }

  /**
   * Turn the access code returned from an OAuth request into a token for
   * branding into each HTTP request to the server.
   *
   * @param code The access code from the user's approval.
   */
  String exchangeCodeForAccessToken(String code) throws IOException {
    try {
      // exchange code for an access token
      GoogleAuthorizationCodeGrant request =
        new GoogleAuthorizationCodeGrant(new NetHttpTransport(),
            new JacksonFactory(),
            clientIdentity.clientId,
            clientIdentity.clientSecret,
            code,
            localServerUrl);
      AccessTokenResponse response = request.execute();
      return response.accessToken;
    } catch (HttpResponseException e) {
      AccessTokenErrorResponse response = e.response.parseAs(AccessTokenErrorResponse.class);
      System.out.println();
      System.err.println("Error: " + response.error);
      System.out.println();
    }
    return null;
  }

  /**
   * Launch a browser to display a URL.
   *
   * If Desktop.BROWSE is not supported, it falls back to printing a URL on
   * the console.
   *
   * @param url The URL to display.
   */
  private static void launchInBrowser(String url) {
    // launch in browser
    boolean browsed = false;
    if (Desktop.isDesktopSupported()) {
      Desktop desktop = Desktop.getDesktop();
      if (desktop.isSupported(Action.BROWSE)) {
        try {
          desktop.browse(URI.create(url));
          browsed = true;
        } catch (Exception e) {
          System.out.println();
          System.err.println("Error: " + e.getMessage());
          System.out.println();
        }
      }
    }
    // Fallback to running Chrome.  Works only in Linux
    if (!browsed) {
      try {
        String browser = "google-chrome";
        Runtime.getRuntime().exec(new String[] {browser, url});
        browsed = true;
      } catch (IOException e) {
        // ignore and fall through
      }
    }
    // Fallback to command line
    if (!browsed) {
      System.out.println("Open the following URL in your favorite browser:");
      System.out.println("  " + url);
    }
  }

  private static void Log(String s) {
    System.out.println("");
    System.out.println(s);
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
    }
  }
}
